Android 使用 Virtual Display 实现屏幕录制
录屏是日常手机使用中的常用问题,在 Android 系统下,系统为开发者提供了录屏相关的底层 API,基于这套 API,开发者可以轻松实现录屏功能。在本文中,通过阅读 ScreenRecordHelper 开源项目,梳理如何实现录屏功能,以及背后的相关知识。
录屏功能依赖一个非常重要的系统概念——Android VirtualDisplay。在 Android 系统中,存在显示屏(Display)的概念,手机自带的屏幕是内置屏幕,有的手机支持连显示器,外接显示器为手机的外部屏幕。但是,Android 系统还支持开发者创建虚拟屏幕。顾名思义,这个屏幕并不实际存在,它位于内存中。录屏功能则基于这一特性。
录屏特性,需要在 Android 5.0(Lollipop)及以上版本中才会支持。Anyway,在 2024 年,这已经不重要了。Android 5.0 以下手机早已成古董。
本文后续内容,以阅读 ScreenRecordHelper 开源项目作为主线,站在开发者角度,按照实现一个录屏 App 的过程讲述。
目标 App
我们将实现如下 App,点击 START 开始录屏,点击 STOP 结束录屏,并保存到文件:
注:图片为 ScreenRecordHelper 项目运行后截图
MainActivity
ScreenRecordHelper 是一个库,先将其视为一个黑箱,来到实例 App 的主界面,看从上层是如何使用这个库的。
核心成员:
// 库实例
private var screenRecordHelper: ScreenRecordHelper? = null
// Assets 目录下内置了一段音乐,最终与视频一同合成保存
private val afdd: AssetFileDescriptor by lazy { assets.openFd("test.aac") }
启动录制
在 START 按钮的点击回调中,对 screenRecordHelper 初始化并启动录制。START 和 STOP 的点击回调均在 onCreate 中设置:
btnStart.setOnClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (screenRecordHelper == null) {
screenRecordHelper = ScreenRecordHelper(this, object : ScreenRecordHelper.OnVideoRecordListener {
override fun onBeforeRecord() {}
override fun onStartRecord() {
play()
}
override fun onCancelRecord() {
releasePlayer()
}
override fun onEndRecord() {
releasePlayer()
}
}, PathUtils.getExternalStoragePath() + "/nanchen")
}
screenRecordHelper?.apply {
if (!isRecording) {
startRecord()
}
}
} else {
Toast.makeText(this@MainActivity.applicationContext, "sorry,your phone does not support recording screen", Toast.LENGTH_LONG).show()
}
}
其中,创建好 ScreenRecordHelper 实例后,调用 ScreenRecordHelper 的 startRecord 开始录制。
创建实例时,ScreenRecordHelper 允许加入一个回调,监听录制生命周期。其中在开始、取消、结束时,都加入了额外操作,主要是用于播放 Assets 目录下的那段音乐文件。
停止录制
看停止录制的回调:
btnStop.setOnClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
screenRecordHelper?.apply {
if (isRecording) {
// 如果选择带参数的 stop 方法,则录制音频无效
stopRecord(mediaPlayer!!.duration.toLong(), 15 * 1000, afdd)
}
}
}
}
其中,还是调用 ScreenRecordHelper 对应的 stopRecord 方法完成。
权限授予
ScreenRecordHelper 内部会申请权限,在 MainActivity 的 onActivityResult 中进行接收。MainActivity 只管接收,收到后准发到 ScreenRecordHelper 内部:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && data != null) {
screenRecordHelper?.onActivityResult(requestCode, resultCode, data)
}
}
ScreenRecordHelper 类
应用层了解完毕后,接下来进入 ScreenRecordHelper 库内部,该库内部只有一个类 ScreenRecordHelper。
核心成员
ScreenRecordHelper 核心成员如下:
// Android 系统服务,用于捕获设备的屏幕内容和/或音频。
private val mediaProjectionManager by lazy { activity.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as? MediaProjectionManager }
// 它用于录制音频和视频
private var mediaRecorder: MediaRecorder? = null
// 它用于从 MediaProjectionManager 获取一个屏幕捕获会话。
private var mediaProjection: MediaProjection? = null
// 虚拟屏幕,用于镜像屏幕内容,用于屏幕捕获
private var virtualDisplay: VirtualDisplay? = null
// 获取屏幕的尺寸和密度信息
private val displayMetrics by lazy { DisplayMetrics() }
// 录制的视频文件
private var saveFile: File? = null
// 表示是否正在录制
var isRecording = false
// 表示是否录制音频
var recordAudio = false
其中,MediaProjection
是 Android 5.0(Lollipop)及以上版本中引入的一个类,它可以捕获设备的屏幕内容和/或音频。这个类的实例不能直接创建,而是通过 MediaProjectionManager
的 getMediaProjection
方法获取。
startRecord 开始录制
具体实现如下:
fun startRecord() {
//...
// 申请存储和麦克风权限
PermissionUtils.permission(PermissionConstants.STORAGE, PermissionConstants.MICROPHONE)
.callback(object : PermissionUtils.SimpleCallback {
override fun onGranted() {
// 权限审批通过
Log.d(TAG, "start record")
// 接下来申请视频录制权限
mediaProjectionManager?.apply {
// 给上层业务的回调通知
listener?.onBeforeRecord()
val intent = this.createScreenCaptureIntent()
if (activity.packageManager.resolveActivity(
intent,
PackageManager.MATCH_DEFAULT_ONLY) != null) {
activity.startActivityForResult(intent, REQUEST_CODE)
} else {
showToast(R.string.phone_not_support_screen_record)
}
}
}
//...
})
.request()
}
startRecord 中分两步获取权限,首先申请存储和录音权限,通过后再次申请录屏权限。其中,录屏权限的 intent 通过 PermissionUtils 开源库的 createScreenCaptureIntent 方法创建。
当录屏权限也审批通过后,来到 onActivityResult,进行实际的录屏操作:
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
if (requestCode == REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
// 获取一个 `MediaProjection` 对象,这个对象用于捕获屏幕内容。
mediaProjection = mediaProjectionManager!!.getMediaProjection(resultCode, data)
// 实测,部分手机上录制视频的时候会有弹窗的出现
Handler().postDelayed({
if (initRecorder()) {
isRecording = true
mediaRecorder?.start()
listener?.onStartRecord()
} else {
showToast(R.string.phone_not_support_screen_record)
}
}, 150)
} else {
showToast(R.string.phone_not_support_screen_record)
}
}
}
initRecorder
首先,initRecorder 执行初始化:
private fun initRecorder(): Boolean {
Log.d(TAG, "initRecorder")
var result = true
// 创建录制文件
val f = File(savePath)
if (!f.exists()) {
f.mkdirs()
}
// 创建临时文件
saveFile = File(savePath, "$saveName.tmp")
saveFile?.apply {
if (exists()) {
delete()
}
}
// MediaRecorder 是 Android 提供的一个用于音频和视频录制的类。
mediaRecorder = MediaRecorder()
val width = Math.min(displayMetrics.widthPixels, 1080)
val height = Math.min(displayMetrics.heightPixels, 1920)
mediaRecorder?.apply {
// 设置音频
if (recordAudio) {
setAudioSource(MediaRecorder.AudioSource.MIC)
}
// 设置视频源为 Surface。这意味着录制的视频将来自一个 Surface,
// 这个 Surface 可以是任何可以绘制的表面,
// 例如一个 View 或者一个 VirtualDisplay。
setVideoSource(MediaRecorder.VideoSource.SURFACE)
// 设置输出格式为 MPEG_4。这意味着录制的视频将被编码为 MPEG-4 格式。
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
// 设置视频编码器为 H.264。这意味着录制的视频将被编码为 H.264 格式。
setVideoEncoder(MediaRecorder.VideoEncoder.H264)
// 如果 `recordAudio` 为 `true`,则设置音频编码器为 AMR_NB。
// 这意味着录制的音频将被编码为 AMR_NB 格式。
if (recordAudio){
setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
}
// 设置输出文件的路径。
setOutputFile(saveFile!!.absolutePath)
// 设置视频的宽度和高度。
setVideoSize(width, height)
// 设置视频编码的比特率。
setVideoEncodingBitRate(8388608)
// 设置视频的帧率。
setVideoFrameRate(VIDEO_FRAME_RATE)
try {
// 调用 MediaRecorder 的 prepare 进行初始化
prepare()
// 创建一个虚拟显示,用于捕获屏幕内容。
// 返回一个 VirtualDisplay 对象
virtualDisplay = mediaProjection?.createVirtualDisplay(
"MainScreen",
width, height, displayMetrics.densityDpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
// MediaRecorder 的 surface,设置为 `MediaRecorder` 的视频源。
surface, null, null)
Log.d(TAG, "initRecorder 成功")
} catch (e: Exception) {
Log.e(TAG, "IllegalStateException preparing MediaRecorder: ${e.message}")
e.printStackTrace()
result = false
}
}
return result
}
这段代码是在设置 MediaRecorder
对象的配置,以便于录制音频和视频。下面是每行代码的详细解释:
视频源连接成功之后,调用 mediaRecorder?.start()
开始录制。
createVirtualDisplay 入参常量
在 createVirtualDisplay 看到有传入常量 DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR
,参见[1] :
常量 | 说明 |
---|---|
VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR |
当没有内容显示时,允许将内容镜像到专用显示器上 |
VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY |
仅显示此屏幕的内容,不镜像显示其他屏幕的内容。 |
VIRTUAL_DISPLAY_FLAG_PRESENTATION |
创建演示文稿的屏幕。 |
VIRTUAL_DISPLAY_FLAG_PUBLIC |
创建公开的屏幕。 |
VIRTUAL_DISPLAY_FLAG_SECURE |
创建一个安全的屏幕 |
本文作者:Maeiee
本文链接:Android 使用 Virtual Display 实现屏幕录制
版权声明:如无特别声明,本文即为原创文章,版权归 Maeiee 所有,未经允许不得转载!
喜欢我文章的朋友请随缘打赏,鼓励我创作更多更好的作品!